using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;

namespace QuickLaunch
{
    public class FileNameSearch
    {
        #region Properties

        private string query;

        private string rawQuery;

        /// <summary>
        /// The file name query string
        /// </summary>
        public string Query
        {
            get { return this.query; }
            set 
            {
                this.rawQuery = value;
                this.query = PreProcessQuery(value); 
            }
        }

        private List<string> directories;

        /// <summary>
        /// The root directories for the search
        /// </summary>
        public List<string> Directories
        {
            get { return this.directories; }
            set { this.directories = value; }
        }

        private List<string> excludeExts;

        public List<string> ExcludeExts
        {
            get { return this.excludeExts; }
            set { this.excludeExts = value; }
        }

        private int visitDepth = 2;

        /// <summary>
        /// The visit depth from the root directories
        /// </summary>
        public int VisitDepth
        {
            get { return this.visitDepth; }
            set { this.visitDepth = value; }
        }

        #endregion

        #region Constructors

        public FileNameSearch(string query, string dir, int visitDepth)
            : this(query, (List<string>)null, (List<string>)null, visitDepth)
        {
            this.directories.Add(dir);
        }

        public FileNameSearch(List<string> dirs, List<string> excludeExts, int visitDepth)
        {
            this.directories = (dirs == null) ? new List<string>() : dirs;
            this.excludeExts = (excludeExts == null) ? new List<string>() : excludeExts;
            this.visitDepth = visitDepth;
        }

        public FileNameSearch(string query, List<string> dirs, List<string> excludeExts, int visitDepth) 
            : this(dirs, excludeExts, visitDepth)
        {
            this.Query = query;
        }

        public FileNameSearch(List<string> dirs, List<string> excludeExts, int visitDepth, string historyFile)
            : this(dirs, excludeExts, visitDepth)
        {
            this.ApplySearchHistory(historyFile);
        }

        #endregion

        private string PreProcessQuery(string query)
        {
            query = query.Trim().ToLower();

            // replace all the spaces with *
            query = Regex.Replace(query, @"\s+", "*");

            // make sure the query starts and ends with '*'
            if (!query.StartsWith("*"))
                query = "*" + query;
            if (!query.EndsWith("*"))
                query += "*";

            return query;
        }

        #region Search history

        private QuickSetting searchHistory = null;

        public bool ApplySearchHistory(string historyFile)
        {
            if (string.IsNullOrEmpty(historyFile))
                // discard the search history for this file name searcher
                searchHistory = null;
            else
            {
                // apply the history file
                try
                {
                    this.searchHistory = new QuickSetting(historyFile);
                }
                catch
                {
                    return false;
                }
            }
            return true;
        }

        #endregion

        public List<FileSearchResult> Search()
        {
            // initialize the result files colleciton
            List<FileSearchResult> resultFiles = new List<FileSearchResult>();

            // 1. find the files
            FindFiles(this.Directories.ToArray(), this.Query, this.VisitDepth, resultFiles);
            // 2. order the search results
            OrderFiles(resultFiles);

            return resultFiles;
        }

        private void FindFiles(string[] dirs, string query, 
            int visitDepth, List<FileSearchResult> resultFiles)
        {
            string ext;
            foreach (string dir in dirs) // for each dir in the dir collection
            {
                if (!Directory.Exists(dir)) // confirm if the dir is valid
                    continue;

                try
                {
                    // get the matching files in the current directories
                    foreach (string file in Directory.GetFiles(dir, query))
                    {
                        ext = Path.GetExtension(file).ToLower();
                        if (ext.StartsWith(".")) ext = ext.Remove(0, 1);
                        if (!this.ExcludeExts.Contains(ext))
                        {
                            // add the find result into the collection
                            resultFiles.Add(new FileSearchResult(file.ToLower()));
                        }
                    }
                }
                catch
                {
                }

                if (visitDepth > 0) // if is allowed to visit the sub folders
                {
                    // get the sub dirs of the current folder
                    string[] subDirs = Directory.GetDirectories(dir);
                    FindFiles(subDirs, query, visitDepth - 1, resultFiles);
                }
            }
        }

        private void OrderFiles(List<FileSearchResult> files)
        {
            // calculate scores for each file
            foreach (FileSearchResult fileResult in files)
            {
                // rule:
                // 1. if the file name starts with the query, get 2 scores.
                if (fileResult.FileName.StartsWith(rawQuery))
                    fileResult.Score += 2;
                // 2. if the file is an exe or bat or url, get 1 score.
                if (fileResult.FileName.EndsWith("exe") || 
                    fileResult.FileName.EndsWith("bat") ||
                    fileResult.FileName.EndsWith("url") || //
                    fileResult.FileName.EndsWith("lnk"))
                    fileResult.Score += 1;

                // 3. if the search history is applied, get score according to the search history.
                if (this.searchHistory != null)
                {
                    string useTimesStr = this.searchHistory.GetValue(fileResult.FileName);
                    int useTimes = 0;
                    if (!string.IsNullOrEmpty(useTimesStr) && int.TryParse(useTimesStr, out useTimes))
                    {
                        fileResult.Score += (int)Math.Round((double)useTimes / 20.0);
                    }
                }
            }
            // reorder the list according to the score
            files.Sort(FileSearchResult.CompareFileSearchResult);
        }

        public bool HitFile(string fileName)
        {
            return HitFile(fileName, "");
        }

        public bool HitFile(string fileName, string param)
        {
            // get the file name part
            string name = FileNameHelper.GetFileName(fileName);

            // check to see if the file name exists in the history.
            string value = this.searchHistory.GetValue(name);
            if (value == null) // not exist
                this.searchHistory.Add(name, "1");
            else
            {
                // increase the value.
                int times = 1;
                if (int.TryParse(value, out times))
                {
                    times++; // add 1 to the use times
                    this.searchHistory.SetValue(name, times.ToString());
                }
            }

            return Shell.Open(fileName, param);
        }
    }

    public static class FileNameHelper
    {
        /// <summary>
        /// Get the file name from the full file path
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public static string GetFileName(string filePath)
        {
            if (string.IsNullOrEmpty(filePath))
                throw new Exception("Invalid file name");

            string name = filePath;
            int lastSlash = filePath.LastIndexOf('\\');
            if (lastSlash >= filePath.Length - 1)
                throw new Exception("Invalid file name");
            if (lastSlash >= 0)
                name = filePath.Substring(lastSlash + 1, filePath.Length - lastSlash - 1);

            return name;
        }

        public static string GetFileDirectory(string filePath)
        {
            if (string.IsNullOrEmpty(filePath))
                throw new Exception("Invalid file name");

            string path = filePath;
            int lastSlash = filePath.LastIndexOf('\\');
            if (lastSlash == filePath.Length - 1) // end with '/'
                return path;
            if (lastSlash >= 0)
                path = filePath.Substring(0, lastSlash);

            return path;
        }

        /// <summary>
        /// Shorten the full file path, replace the middle part with '...'
        /// </summary>
        /// <param name="filePath"></param>
        /// <returns></returns>
        public static string Shorten(string filePath)
        {
            if (string.IsNullOrEmpty(filePath))
                throw new Exception("Invalid file name");

            string[] parts = filePath.Split('\\');
            if (parts.Length > 4)
                return parts[0] + "\\" + parts[1] + "\\...\\" +
                    parts[parts.Length - 2] + "\\" + parts[parts.Length - 1];
            else
                return filePath;
        }
    }

    public class FileSearchResult
    {
        private string fileName;

        public string FileName
        {
            get { return this.fileName; }
            set { this.fileName = value; }
        }

        private string filePath;

        /// <summary>
        /// File dir + file name
        /// </summary>
        public string FilePath
        {
            get { return this.filePath; }
            set { this.filePath = value; }
        }

        private int score = 0;

        public int Score
        {
            get { return this.score; }
            set { this.score = value; }
        }

        #region Constructors

        public FileSearchResult(string fileName, string path)
        {
            this.fileName = fileName.Trim();
            this.filePath = path.Trim();
        }

        public FileSearchResult(string fullPath)
        {
            this.filePath = fullPath;
            int last = fullPath.LastIndexOf('\\');
            if (last < 0)
                throw new Exception("Invalid file full path: " + fullPath);
            this.fileName = fullPath.Substring(last + 1).Trim();
            if (string.IsNullOrEmpty(this.fileName))
                throw new Exception("Invalid file full path: " + fullPath);
        }

        #endregion

        public static int CompareFileSearchResult(FileSearchResult result1, FileSearchResult result2)
        {
            if (result1 == null)
            {
                if (result2 == null)
                    return 0; // both are null, so they're equal.
                else
                    return -1; // if result1 is null and result2 is not null, result2 is greater. 
            }
            else  // if result1 is not null
            {
                if (result2 == null)
                    return 1; // result2 is null, result1 is greater.
                else
                {
                    return result1.Score.CompareTo(result2.Score);
                }
            }
        }
    }
}
